reqs
python==3.13.0
pandas==2.2.3
notebook==7.3.3
numpy==1.26.4
scikit-learn==1.6.1
sweetviz==2.3.1
lightgbm==4.6.0
optuna-integration[sklearn]==4.2.1
shap==0.47.1
matplotlib==3.10.1
seaborn==0.13.2
Загружаем данные¶
Рассмотрим данные из соревнования по предсказанию стоимости жилья
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import shap
import sklearn
import sweetviz as sv
from sklearn.model_selection import train_test_split
import lightgbm
import optuna
from optuna.distributions import IntDistribution, FloatDistribution, CategoricalDistribution
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import random
import warnings
# Фильтруем маловажные предупреждения
warnings.filterwarnings("ignore", category=UserWarning,
module="sklearn.preprocessing._encoders")
warnings.filterwarnings("ignore", category=UserWarning,
module="sklearn.utils.validation")
# !wget -O 'train.csv' -q 'https://www.dropbox.com/s/6dxq90t0prn2vaw/_train_sem2.csv?dl=0'
# Изначальный код загрузки не сработал, датасет загружен вручную
data = pd.read_csv("train.csv")
data.head()
| Id | MSSubClass | MSZoning | LotFrontage | LotArea | Street | Alley | LotShape | LandContour | Utilities | ... | PoolArea | PoolQC | Fence | MiscFeature | MiscVal | MoSold | YrSold | SaleType | SaleCondition | SalePrice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 60 | RL | 65.0 | 8450 | Pave | NaN | Reg | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 2 | 2008 | WD | Normal | 208500 |
| 1 | 2 | 20 | RL | 80.0 | 9600 | Pave | NaN | Reg | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 5 | 2007 | WD | Normal | 181500 |
| 2 | 3 | 60 | RL | 68.0 | 11250 | Pave | NaN | IR1 | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 9 | 2008 | WD | Normal | 223500 |
| 3 | 4 | 70 | RL | 60.0 | 9550 | Pave | NaN | IR1 | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 2 | 2006 | WD | Abnorml | 140000 |
| 4 | 5 | 60 | RL | 84.0 | 14260 | Pave | NaN | IR1 | Lvl | AllPub | ... | 0 | NaN | NaN | NaN | 0 | 12 | 2008 | WD | Normal | 250000 |
5 rows × 81 columns
Первое, что стоит заметить — у нас в данных есть уникальное для каждого объекта поле id. Обычно такие поля только мешают и способствуют переобучению. Удалим это поле из данных.
Разделим данные на обучающую и тестовую выборки. Для простоты не будем выделять дополнительно валидационную выборку (хотя это обычно стоит делать, она нужна для подбора гиперпараметров модели, то есть параметров, которые нельзя подбирать по обучающей выборке). Дополнительно нам придется отделить значения целевой переменной от данных.
Вопрос 1: Почему поля типа id могут вызвать переобучение модели (не обязательно линейной)?
Ответ: Id не несёт в себе никакой полезной информации. Простые модели будут пытаться найти взаимосвязи, которых нет. А сложные могут попытаться связать id с целевой переменной, что приведёт к полному переобучению.
Вопрос 2: Почему стоит дополнительно отделять валидационную выборку?
Ответ: Валидационная выборка - единственный способ получить промежуточную оценку во время обучения или подбора гиперпараметров. Тестовые данные для этого не подходят, они должны оставаться эталонно-нетронутыми до финального теста, ведь именно на них мы проверяем модель на степень переобучения и общую адекватность.
Вопрос 3: Обратите внимание на фиксацию random_state при сплите данных. Почему это важно?
Ответ: Фиксация рандомности разделения. Это гарантирует что при повторном запуске кода данные разделятся так-же как и в предыдущие разы. То-же относится к некоторым моделям, предусматривающим рандомизацию в расчётах. Без константы случайности результаты экспериментов будут невоспроизводимыми.
# Константа случайности
RANDOM_STATE = 10
Задание¶
- Проведите EDA анализ данных
- Выберите наиболее релевантные факторы для прогноза
- Подберите лучшую модель (используйте в качестве метрики качества показатель RMSE
- Проверьте качество данной модели - на сколько она точна, насколько стабильна, насколько правильно специфирована
Разведочный анализ данных и предобработка¶
Начнём с базового ознакомления с данными. Типы, количество, названия признаков - нет ничего надёжнее .info()
data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1460 entries, 0 to 1459 Data columns (total 81 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Id 1460 non-null int64 1 MSSubClass 1460 non-null int64 2 MSZoning 1460 non-null object 3 LotFrontage 1201 non-null float64 4 LotArea 1460 non-null int64 5 Street 1460 non-null object 6 Alley 91 non-null object 7 LotShape 1460 non-null object 8 LandContour 1460 non-null object 9 Utilities 1460 non-null object 10 LotConfig 1460 non-null object 11 LandSlope 1460 non-null object 12 Neighborhood 1460 non-null object 13 Condition1 1460 non-null object 14 Condition2 1460 non-null object 15 BldgType 1460 non-null object 16 HouseStyle 1460 non-null object 17 OverallQual 1460 non-null int64 18 OverallCond 1460 non-null int64 19 YearBuilt 1460 non-null int64 20 YearRemodAdd 1460 non-null int64 21 RoofStyle 1460 non-null object 22 RoofMatl 1460 non-null object 23 Exterior1st 1460 non-null object 24 Exterior2nd 1460 non-null object 25 MasVnrType 588 non-null object 26 MasVnrArea 1452 non-null float64 27 ExterQual 1460 non-null object 28 ExterCond 1460 non-null object 29 Foundation 1460 non-null object 30 BsmtQual 1423 non-null object 31 BsmtCond 1423 non-null object 32 BsmtExposure 1422 non-null object 33 BsmtFinType1 1423 non-null object 34 BsmtFinSF1 1460 non-null int64 35 BsmtFinType2 1422 non-null object 36 BsmtFinSF2 1460 non-null int64 37 BsmtUnfSF 1460 non-null int64 38 TotalBsmtSF 1460 non-null int64 39 Heating 1460 non-null object 40 HeatingQC 1460 non-null object 41 CentralAir 1460 non-null object 42 Electrical 1459 non-null object 43 1stFlrSF 1460 non-null int64 44 2ndFlrSF 1460 non-null int64 45 LowQualFinSF 1460 non-null int64 46 GrLivArea 1460 non-null int64 47 BsmtFullBath 1460 non-null int64 48 BsmtHalfBath 1460 non-null int64 49 FullBath 1460 non-null int64 50 HalfBath 1460 non-null int64 51 BedroomAbvGr 1460 non-null int64 52 KitchenAbvGr 1460 non-null int64 53 KitchenQual 1460 non-null object 54 TotRmsAbvGrd 1460 non-null int64 55 Functional 1460 non-null object 56 Fireplaces 1460 non-null int64 57 FireplaceQu 770 non-null object 58 GarageType 1379 non-null object 59 GarageYrBlt 1379 non-null float64 60 GarageFinish 1379 non-null object 61 GarageCars 1460 non-null int64 62 GarageArea 1460 non-null int64 63 GarageQual 1379 non-null object 64 GarageCond 1379 non-null object 65 PavedDrive 1460 non-null object 66 WoodDeckSF 1460 non-null int64 67 OpenPorchSF 1460 non-null int64 68 EnclosedPorch 1460 non-null int64 69 3SsnPorch 1460 non-null int64 70 ScreenPorch 1460 non-null int64 71 PoolArea 1460 non-null int64 72 PoolQC 7 non-null object 73 Fence 281 non-null object 74 MiscFeature 54 non-null object 75 MiscVal 1460 non-null int64 76 MoSold 1460 non-null int64 77 YrSold 1460 non-null int64 78 SaleType 1460 non-null object 79 SaleCondition 1460 non-null object 80 SalePrice 1460 non-null int64 dtypes: float64(3), int64(35), object(43) memory usage: 924.0+ KB
80 признаков, 1460 записей. Сразу видно наличие пропусков. Проведём более глубокий EDA автоматизированными инструментами.
sv.analyze(data, target_feat='SalePrice').show_notebook()
| | [ 0%] 00:00 ->…
80 признаков - это больше, чем нам необходимо. Воспользуемся матрицей ассоциаций, чтобы выделить из них наименее информативные, и удалим их.
# data = data.loc[:, data.columns.isin(["SalePrice", "Neighborhood", "OverallQual", "YearBuilt", "YearRemodAdd", "Exterior1st", "Exterior2nd", "MasVnrType", "MasVnrArea", "ExterQual", "Foundation", "BsmtQual", "BsmtFinType1", "TotalBsmtSF", "HeatingQC", "1stFlrSF", "GrLivArea", "FullBath", "KitchenQual", "TotRmsAbvGrd", "Fireplaces", "FireplaceQu", "GarageType", "GarageYrBlt", "GarageFinish", "GarageCars", "GarageArea"])]
# Убираем все признаки, кроме наиболее информативных(Неэффективно. Низкие оценки на кросс-валидации)
data = data.drop(["YrSold", "MiscVal", "MiscFeature", "ScreenPorch", "3SsnPorch", "EnclosedPorch", "Functional", "BsmtHalfBath", "LowQualFinSF", "Heating", "BsmtFinSF2", "ExterCond", "LandSlope", "Utilities", "Street", "MSSubClass", "Id"], axis=1)
# Уберём наименее информативные признаки
y = data["SalePrice"] # целевая переменная (target)
X = data.drop(columns=["SalePrice"]) # из пространства признаков выкинули целевую переменную
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=RANDOM_STATE)
Теперь займёмся препроцессингом. Для ускорения работы с данными сделаем это в пайплайне. Воспользуемся ColumnTransformer, чтобы распределить данные на скейлинг и энкодинг. Попутно заполним отсутствующие значения, у численных данных медианой, у категорий самым часто-встречающимся значением
preprocessor = sklearn.compose.ColumnTransformer([
('ohe', Pipeline([
('impute', sklearn.impute.SimpleImputer(strategy='most_frequent')),
('encode', sklearn.preprocessing.OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False))
]), \
X_train.select_dtypes(exclude='number').columns
),
('scaler', Pipeline([
('impute', sklearn.impute.SimpleImputer(strategy='median')),
('scale', sklearn.preprocessing.StandardScaler())
]), \
X_train.select_dtypes(include='number').columns
)
],
remainder='passthrough'
)
Итого, в рамках разведочного анализа и предобработки, мы:
- Ознакомились с признаками, выявили наличие пропусков и отсутствие ярких аномалий;
- Выявили и удалили малоинформативные признаки при помощи матрицы ассоциаций;
- Провели разделение данных на тренировочную и тестовую выборки
- Подготовили препроцессор для будущих пайплайнов модели, включающий - заполнение пропусков, скейлинг и кодирование признаков.
Обучение моделей¶
Теперь приступим к обучению моделей. Выбирая кандидатов постараемся уделить внимание моделям разнообразной сложности. Никогда не знаешь, когда линейная регрессия покажет себя лучше нейросети.
За представителя сравнительно простой модели возьмём метод опорных векторов с линейным ядром. Обучать будем сразу с подбором гиперпараметров и кросс-валидацией. За метрику возьмём корень среднего квадрата ошибки
svm = Pipeline([
('preprocess', preprocessor),
('model', sklearn.svm.SVR(kernel='linear'))
])
param_distribution_svm = {
"model__C": FloatDistribution(1e-3, 1e3, log=True),
"model__gamma": FloatDistribution(1e-4, 1e1, log=True),
"model__epsilon": FloatDistribution(0.01, 0.2)
}
svm_cv = optuna.integration.OptunaSearchCV(svm, param_distributions=param_distribution_svm, cv=5, scoring='neg_root_mean_squared_error', random_state=RANDOM_STATE, n_trials=10)
svm_cv.fit(X_train, y_train)
print('RMSE:',cross_val_score(svm_cv.best_estimator_, X_train, y_train, scoring='neg_root_mean_squared_error').mean()*-1)
C:\Users\m8913\AppData\Local\Temp\ipykernel_2812\584707306.py:12: ExperimentalWarning: OptunaSearchCV is experimental (supported from v0.17.0). The interface can change in the future.
svm_cv = optuna.integration.OptunaSearchCV(svm, param_distributions=param_distribution_svm, cv=5, scoring='neg_root_mean_squared_error', random_state=RANDOM_STATE, n_trials=10)
[I 2025-03-27 11:54:47,738] A new study created in memory with name: no-name-1aed3424-c180-406b-aae1-e98abea44bbf
[I 2025-03-27 11:54:48,146] Trial 0 finished with value: -81888.26082198271 and parameters: {'model__C': 0.003585583615585772, 'model__gamma': 1.0476834405905207, 'model__epsilon': 0.06961979194105183}. Best is trial 0 with value: -81888.26082198271.
[I 2025-03-27 11:54:48,534] Trial 1 finished with value: -81802.6808220816 and parameters: {'model__C': 0.024412890980453777, 'model__gamma': 0.001593717567647103, 'model__epsilon': 0.08915874944020485}. Best is trial 1 with value: -81802.6808220816.
[I 2025-03-27 11:54:48,934] Trial 2 finished with value: -41779.82353450205 and parameters: {'model__C': 38.76749114249326, 'model__gamma': 1.56933184557578, 'model__epsilon': 0.055994602251620215}. Best is trial 2 with value: -41779.82353450205.
[I 2025-03-27 11:54:49,344] Trial 3 finished with value: -39294.46044221624 and parameters: {'model__C': 69.43953913944551, 'model__gamma': 8.048026506705487, 'model__epsilon': 0.01659496624572367}. Best is trial 3 with value: -39294.46044221624.
[I 2025-03-27 11:54:49,728] Trial 4 finished with value: -81822.4227480425 and parameters: {'model__C': 0.019682192940159417, 'model__gamma': 0.00043463729875164016, 'model__epsilon': 0.08245164838984852}. Best is trial 3 with value: -39294.46044221624.
[I 2025-03-27 11:54:50,147] Trial 5 finished with value: -37164.61725644615 and parameters: {'model__C': 152.44208781402085, 'model__gamma': 0.8641843670856602, 'model__epsilon': 0.04089259098446621}. Best is trial 5 with value: -37164.61725644615.
[I 2025-03-27 11:54:50,531] Trial 6 finished with value: -79841.13341517554 and parameters: {'model__C': 0.46476358312751725, 'model__gamma': 0.05924000380710417, 'model__epsilon': 0.11557320544596486}. Best is trial 5 with value: -37164.61725644615.
[I 2025-03-27 11:54:50,924] Trial 7 finished with value: -41776.58068211471 and parameters: {'model__C': 38.7932306032779, 'model__gamma': 0.0032733675353925854, 'model__epsilon': 0.13195667074229078}. Best is trial 5 with value: -37164.61725644615.
[I 2025-03-27 11:54:51,308] Trial 8 finished with value: -58007.32705242101 and parameters: {'model__C': 7.296970093622444, 'model__gamma': 0.007895922728940888, 'model__epsilon': 0.10825492480306731}. Best is trial 5 with value: -37164.61725644615.
[I 2025-03-27 11:54:51,697] Trial 9 finished with value: -45634.59497420418 and parameters: {'model__C': 21.51898696926201, 'model__gamma': 0.1207534873981832, 'model__epsilon': 0.07538472013223435}. Best is trial 5 with value: -37164.61725644615.
RMSE: 37164.61725644615
Этот результат не предел совершенства, поэтому мы попробуем его улучшить.
Воспользуемся более сложной моделью - лёгким градиентным бустингом. Точно так-же подберём гиперпараметры и проверим качество лучшей модели на кросс-валидации.
lgbm = Pipeline([
('preprocess', preprocessor),
('model', lightgbm.LGBMRegressor(random_state=RANDOM_STATE, verbose=-1, objective='poisson', boosting_type='gbdt'))
])
param_distribution_lgbm = {
'model__num_leaves': IntDistribution(low=20, high=150, step=1),
'model__max_depth': IntDistribution(low=3, high=15, step=1),
'model__learning_rate': FloatDistribution(low=0.01, high=0.3, log=False),
'model__n_estimators': IntDistribution(low=50, high=500, step=50),
'model__min_child_samples': IntDistribution(low=5, high=100, step=5),
'model__colsample_bytree': FloatDistribution(low=0.6, high=1.0),
'model__reg_alpha': FloatDistribution(low=0, high=1),
'model__reg_lambda': FloatDistribution(low=0, high=1)
}
lgbm_cv = optuna.integration.OptunaSearchCV(lgbm, param_distributions=param_distribution_lgbm, cv=5, scoring='neg_root_mean_squared_error', random_state=RANDOM_STATE, n_trials=10)
lgbm_cv.fit(X_train, y_train)
print('RMSE:', cross_val_score(lgbm_cv.best_estimator_, X_train, y_train, scoring='neg_root_mean_squared_error').mean()*-1)
C:\Users\m8913\AppData\Local\Temp\ipykernel_2812\2730049852.py:17: ExperimentalWarning: OptunaSearchCV is experimental (supported from v0.17.0). The interface can change in the future.
lgbm_cv = optuna.integration.OptunaSearchCV(lgbm, param_distributions=param_distribution_lgbm, cv=5, scoring='neg_root_mean_squared_error', random_state=RANDOM_STATE, n_trials=10)
[I 2025-03-27 11:54:52,208] A new study created in memory with name: no-name-d6b4a832-6f45-4e2e-945e-7dd9a41f4e17
[I 2025-03-27 11:54:53,833] Trial 0 finished with value: -28587.832084951377 and parameters: {'model__num_leaves': 32, 'model__max_depth': 13, 'model__learning_rate': 0.10099862980476332, 'model__n_estimators': 150, 'model__min_child_samples': 25, 'model__colsample_bytree': 0.7666499988214839, 'model__reg_alpha': 0.7647446160554672, 'model__reg_lambda': 0.8391429575464384}. Best is trial 0 with value: -28587.832084951377.
[I 2025-03-27 11:54:54,503] Trial 1 finished with value: -29219.8913615595 and parameters: {'model__num_leaves': 51, 'model__max_depth': 13, 'model__learning_rate': 0.29452998508931827, 'model__n_estimators': 50, 'model__min_child_samples': 25, 'model__colsample_bytree': 0.6510501594333025, 'model__reg_alpha': 0.38132446520972907, 'model__reg_lambda': 0.863850814690767}. Best is trial 0 with value: -28587.832084951377.
[I 2025-03-27 11:54:55,650] Trial 2 finished with value: -30877.194067645927 and parameters: {'model__num_leaves': 123, 'model__max_depth': 5, 'model__learning_rate': 0.13891621774473858, 'model__n_estimators': 300, 'model__min_child_samples': 60, 'model__colsample_bytree': 0.9059170632043324, 'model__reg_alpha': 0.30299895417076916, 'model__reg_lambda': 0.6418772144331093}. Best is trial 0 with value: -28587.832084951377.
[I 2025-03-27 11:54:57,423] Trial 3 finished with value: -31027.061265321106 and parameters: {'model__num_leaves': 104, 'model__max_depth': 7, 'model__learning_rate': 0.15996804312047117, 'model__n_estimators': 400, 'model__min_child_samples': 65, 'model__colsample_bytree': 0.7376520423836512, 'model__reg_alpha': 0.1703168406424409, 'model__reg_lambda': 0.8387758324726942}. Best is trial 0 with value: -28587.832084951377.
[I 2025-03-27 11:54:58,743] Trial 4 finished with value: -30456.48701006016 and parameters: {'model__num_leaves': 38, 'model__max_depth': 5, 'model__learning_rate': 0.14171292000913205, 'model__n_estimators': 350, 'model__min_child_samples': 55, 'model__colsample_bytree': 0.8754220091967231, 'model__reg_alpha': 0.5712981963943911, 'model__reg_lambda': 0.8074201583678433}. Best is trial 0 with value: -28587.832084951377.
[I 2025-03-27 11:55:00,401] Trial 5 finished with value: -27320.849053227634 and parameters: {'model__num_leaves': 133, 'model__max_depth': 5, 'model__learning_rate': 0.08794560715288294, 'model__n_estimators': 400, 'model__min_child_samples': 30, 'model__colsample_bytree': 0.6689929656509956, 'model__reg_alpha': 0.9148078283231919, 'model__reg_lambda': 0.3579902570947734}. Best is trial 5 with value: -27320.849053227634.
[I 2025-03-27 11:55:01,314] Trial 6 finished with value: -32061.252342346103 and parameters: {'model__num_leaves': 72, 'model__max_depth': 11, 'model__learning_rate': 0.0674612201494978, 'model__n_estimators': 250, 'model__min_child_samples': 100, 'model__colsample_bytree': 0.7133797847536105, 'model__reg_alpha': 0.9694825133296966, 'model__reg_lambda': 0.2744572662383924}. Best is trial 5 with value: -27320.849053227634.
[I 2025-03-27 11:55:01,804] Trial 7 finished with value: -30662.55262974088 and parameters: {'model__num_leaves': 24, 'model__max_depth': 9, 'model__learning_rate': 0.23520434115270308, 'model__n_estimators': 50, 'model__min_child_samples': 45, 'model__colsample_bytree': 0.950913157219048, 'model__reg_alpha': 0.10602220644538718, 'model__reg_lambda': 0.5953372470432962}. Best is trial 5 with value: -27320.849053227634.
[I 2025-03-27 11:55:04,737] Trial 8 finished with value: -27870.369885998673 and parameters: {'model__num_leaves': 31, 'model__max_depth': 9, 'model__learning_rate': 0.2832730822994129, 'model__n_estimators': 400, 'model__min_child_samples': 25, 'model__colsample_bytree': 0.6554401204582899, 'model__reg_alpha': 0.928746315106096, 'model__reg_lambda': 0.07568300910027659}. Best is trial 5 with value: -27320.849053227634.
[I 2025-03-27 11:55:05,612] Trial 9 finished with value: -34309.37470366442 and parameters: {'model__num_leaves': 76, 'model__max_depth': 14, 'model__learning_rate': 0.0810279534911221, 'model__n_estimators': 50, 'model__min_child_samples': 20, 'model__colsample_bytree': 0.6242388416157856, 'model__reg_alpha': 0.5766428589497673, 'model__reg_lambda': 0.5484028646309496}. Best is trial 5 with value: -27320.849053227634.
RMSE: 27320.849053227634
Значительно лучше, но есть куда совершенствоваться.
Следующая модель - случайный лес.
forest = Pipeline([
('preprocess', preprocessor),
('model', sklearn.ensemble.RandomForestRegressor(random_state=RANDOM_STATE, n_jobs=-1))
])
param_distribution_forest = {
"model__n_estimators": IntDistribution(50, 500, step=50),
"model__max_depth": IntDistribution(3, 15),
"model__min_samples_split": IntDistribution(2, 20),
"model__min_samples_leaf": IntDistribution(1, 10),
"model__criterion": CategoricalDistribution(["squared_error", "absolute_error", "friedman_mse"]),
"model__max_features": CategoricalDistribution(["sqrt", "log2", None]),
"model__min_impurity_decrease": FloatDistribution(0.0, 0.5, step=0.01),
"model__max_samples": FloatDistribution(0.5, 1.0)
}
forest_cv = optuna.integration.OptunaSearchCV(forest, param_distributions=param_distribution_forest, cv=5, scoring='neg_root_mean_squared_error', random_state=RANDOM_STATE, n_trials=10)
forest_cv.fit(X_train, y_train)
print('RMSE:',cross_val_score(forest_cv.best_estimator_, X_train, y_train, scoring='neg_root_mean_squared_error').mean()*-1)
C:\Users\m8913\AppData\Local\Temp\ipykernel_2812\191560939.py:17: ExperimentalWarning: OptunaSearchCV is experimental (supported from v0.17.0). The interface can change in the future.
forest_cv = optuna.integration.OptunaSearchCV(forest, param_distributions=param_distribution_forest, cv=5, scoring='neg_root_mean_squared_error', random_state=RANDOM_STATE, n_trials=10)
[I 2025-03-27 11:55:07,708] A new study created in memory with name: no-name-f5c5e1e7-5162-4ce2-b45a-32e326e9e095
[I 2025-03-27 11:55:08,464] Trial 0 finished with value: -36120.24060736828 and parameters: {'model__n_estimators': 50, 'model__max_depth': 13, 'model__min_samples_split': 7, 'model__min_samples_leaf': 3, 'model__criterion': 'friedman_mse', 'model__max_features': 'sqrt', 'model__min_impurity_decrease': 0.5, 'model__max_samples': 0.5173551743308518}. Best is trial 0 with value: -36120.24060736828.
[I 2025-03-27 11:55:09,691] Trial 1 finished with value: -36811.91872406435 and parameters: {'model__n_estimators': 150, 'model__max_depth': 4, 'model__min_samples_split': 9, 'model__min_samples_leaf': 9, 'model__criterion': 'squared_error', 'model__max_features': None, 'model__min_impurity_decrease': 0.15, 'model__max_samples': 0.8209386072165546}. Best is trial 0 with value: -36120.24060736828.
[I 2025-03-27 11:55:11,979] Trial 2 finished with value: -38094.95893125291 and parameters: {'model__n_estimators': 350, 'model__max_depth': 7, 'model__min_samples_split': 11, 'model__min_samples_leaf': 8, 'model__criterion': 'squared_error', 'model__max_features': 'sqrt', 'model__min_impurity_decrease': 0.23, 'model__max_samples': 0.8145512328599559}. Best is trial 0 with value: -36120.24060736828.
[I 2025-03-27 11:55:13,966] Trial 3 finished with value: -38777.06476564985 and parameters: {'model__n_estimators': 300, 'model__max_depth': 11, 'model__min_samples_split': 12, 'model__min_samples_leaf': 9, 'model__criterion': 'squared_error', 'model__max_features': 'sqrt', 'model__min_impurity_decrease': 0.46, 'model__max_samples': 0.6789951285473868}. Best is trial 0 with value: -36120.24060736828.
[I 2025-03-27 11:55:15,741] Trial 4 finished with value: -33297.41443086666 and parameters: {'model__n_estimators': 250, 'model__max_depth': 11, 'model__min_samples_split': 5, 'model__min_samples_leaf': 5, 'model__criterion': 'friedman_mse', 'model__max_features': None, 'model__min_impurity_decrease': 0.39, 'model__max_samples': 0.5476424157409567}. Best is trial 4 with value: -33297.41443086666.
[I 2025-03-27 11:55:17,476] Trial 5 finished with value: -37474.18864279444 and parameters: {'model__n_estimators': 250, 'model__max_depth': 14, 'model__min_samples_split': 4, 'model__min_samples_leaf': 6, 'model__criterion': 'friedman_mse', 'model__max_features': 'sqrt', 'model__min_impurity_decrease': 0.47000000000000003, 'model__max_samples': 0.5378415045501383}. Best is trial 4 with value: -33297.41443086666.
[I 2025-03-27 11:55:19,255] Trial 6 finished with value: -32145.728974372254 and parameters: {'model__n_estimators': 250, 'model__max_depth': 14, 'model__min_samples_split': 6, 'model__min_samples_leaf': 1, 'model__criterion': 'friedman_mse', 'model__max_features': None, 'model__min_impurity_decrease': 0.04, 'model__max_samples': 0.5499841742828928}. Best is trial 6 with value: -32145.728974372254.
[I 2025-03-27 11:55:19,906] Trial 7 finished with value: -40366.31495235264 and parameters: {'model__n_estimators': 50, 'model__max_depth': 10, 'model__min_samples_split': 6, 'model__min_samples_leaf': 5, 'model__criterion': 'squared_error', 'model__max_features': 'log2', 'model__min_impurity_decrease': 0.29, 'model__max_samples': 0.8355538352829297}. Best is trial 6 with value: -32145.728974372254.
[I 2025-03-27 11:55:20,865] Trial 8 finished with value: -44818.00052270107 and parameters: {'model__n_estimators': 100, 'model__max_depth': 9, 'model__min_samples_split': 17, 'model__min_samples_leaf': 7, 'model__criterion': 'absolute_error', 'model__max_features': 'log2', 'model__min_impurity_decrease': 0.35000000000000003, 'model__max_samples': 0.8342282217075911}. Best is trial 6 with value: -32145.728974372254.
[I 2025-03-27 11:55:23,938] Trial 9 finished with value: -42702.66284832203 and parameters: {'model__n_estimators': 500, 'model__max_depth': 15, 'model__min_samples_split': 6, 'model__min_samples_leaf': 10, 'model__criterion': 'squared_error', 'model__max_features': 'log2', 'model__min_impurity_decrease': 0.4, 'model__max_samples': 0.9572046439258426}. Best is trial 6 with value: -32145.728974372254.
RMSE: 32145.728974372254
Случайный лес справился хуже градиентного бустинга.
И наш победитель - Лёгкий градиентный бустинг. Далее на очереди итоговое тестирование.
На этом завершается обучение моделей. В рамках обучения, мы:
- Подготовили пайплайны с препроцессингом для трёх моделей машинного обучения - SVM, LGBM и RandomForest
- Провели подбор гиперпараметров на OptunaSearchCV
- Провели кросс-валидацию моделей с лучшими параметрами и выявили лучшую модель - Лёгкий градиентный бустинг.
Тестирование качества лучшей модели¶
Выбор лучшей модели был сделан, а значит больше нам не пригодятся по-отдельности тренировочные и валидационные данные. Можем их все скормить лучшей модели.
lgbm_cv.best_estimator_.fit(X_train, y_train)
Pipeline(steps=[('preprocess',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('impute',
SimpleImputer(strategy='most_frequent')),
('encode',
OneHotEncoder(drop='first',
handle_unknown='ignore',
sparse_output=False))]),
Index(['MSZoning', 'Alley', 'LotShape', 'LandContour', 'LotConfig',
'Neighborhood', 'Condition1', 'Condition2', '...
'Fireplaces', 'GarageYrBlt', 'GarageCars', 'GarageArea', 'WoodDeckSF',
'OpenPorchSF', 'PoolArea', 'MoSold'],
dtype='object'))])),
('model',
LGBMRegressor(colsample_bytree=0.6689929656509956,
learning_rate=0.08794560715288294, max_depth=5,
min_child_samples=30, n_estimators=400,
num_leaves=133, objective='poisson',
random_state=10, reg_alpha=0.9148078283231919,
reg_lambda=0.3579902570947734, verbose=-1))])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('preprocess',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('impute',
SimpleImputer(strategy='most_frequent')),
('encode',
OneHotEncoder(drop='first',
handle_unknown='ignore',
sparse_output=False))]),
Index(['MSZoning', 'Alley', 'LotShape', 'LandContour', 'LotConfig',
'Neighborhood', 'Condition1', 'Condition2', '...
'Fireplaces', 'GarageYrBlt', 'GarageCars', 'GarageArea', 'WoodDeckSF',
'OpenPorchSF', 'PoolArea', 'MoSold'],
dtype='object'))])),
('model',
LGBMRegressor(colsample_bytree=0.6689929656509956,
learning_rate=0.08794560715288294, max_depth=5,
min_child_samples=30, n_estimators=400,
num_leaves=133, objective='poisson',
random_state=10, reg_alpha=0.9148078283231919,
reg_lambda=0.3579902570947734, verbose=-1))])ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('impute',
SimpleImputer(strategy='most_frequent')),
('encode',
OneHotEncoder(drop='first',
handle_unknown='ignore',
sparse_output=False))]),
Index(['MSZoning', 'Alley', 'LotShape', 'LandContour', 'LotConfig',
'Neighborhood', 'Condition1', 'Condition2', 'BldgType', 'HouseStyle',
'RoofSt...
Index(['LotFrontage', 'LotArea', 'OverallQual', 'OverallCond', 'YearBuilt',
'YearRemodAdd', 'MasVnrArea', 'BsmtFinSF1', 'BsmtUnfSF', 'TotalBsmtSF',
'1stFlrSF', '2ndFlrSF', 'GrLivArea', 'BsmtFullBath', 'FullBath',
'HalfBath', 'BedroomAbvGr', 'KitchenAbvGr', 'TotRmsAbvGrd',
'Fireplaces', 'GarageYrBlt', 'GarageCars', 'GarageArea', 'WoodDeckSF',
'OpenPorchSF', 'PoolArea', 'MoSold'],
dtype='object'))])Index(['MSZoning', 'Alley', 'LotShape', 'LandContour', 'LotConfig',
'Neighborhood', 'Condition1', 'Condition2', 'BldgType', 'HouseStyle',
'RoofStyle', 'RoofMatl', 'Exterior1st', 'Exterior2nd', 'MasVnrType',
'ExterQual', 'Foundation', 'BsmtQual', 'BsmtCond', 'BsmtExposure',
'BsmtFinType1', 'BsmtFinType2', 'HeatingQC', 'CentralAir', 'Electrical',
'KitchenQual', 'FireplaceQu', 'GarageType', 'GarageFinish',
'GarageQual', 'GarageCond', 'PavedDrive', 'PoolQC', 'Fence', 'SaleType',
'SaleCondition'],
dtype='object')SimpleImputer(strategy='most_frequent')
OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False)
Index(['LotFrontage', 'LotArea', 'OverallQual', 'OverallCond', 'YearBuilt',
'YearRemodAdd', 'MasVnrArea', 'BsmtFinSF1', 'BsmtUnfSF', 'TotalBsmtSF',
'1stFlrSF', '2ndFlrSF', 'GrLivArea', 'BsmtFullBath', 'FullBath',
'HalfBath', 'BedroomAbvGr', 'KitchenAbvGr', 'TotRmsAbvGrd',
'Fireplaces', 'GarageYrBlt', 'GarageCars', 'GarageArea', 'WoodDeckSF',
'OpenPorchSF', 'PoolArea', 'MoSold'],
dtype='object')SimpleImputer(strategy='median')
StandardScaler()
[]
passthrough
LGBMRegressor(colsample_bytree=0.6689929656509956,
learning_rate=0.08794560715288294, max_depth=5,
min_child_samples=30, n_estimators=400, num_leaves=133,
objective='poisson', random_state=10,
reg_alpha=0.9148078283231919, reg_lambda=0.3579902570947734,
verbose=-1)А теперь приступим к финальным тестам. Начнём с простого просмотра метрик MAE, RMSE, R2 и MAPE.
y_pred = lgbm_cv.best_estimator_.predict(X_test)
metrics = {
"MAE": f"{mean_absolute_error(y_test, y_pred):.3f}",
"RMSE": f"{np.sqrt(mean_squared_error(y_test, y_pred)):.3f}",
"R²": f"{r2_score(y_test, y_pred):.3f}",
"MAPE": f"{np.mean(np.abs((y_test - y_pred) / y_test)) * 100:.2f}%"
}
pd.DataFrame.from_dict(metrics, orient='index', columns=['Value'])
| Value | |
|---|---|
| MAE | 16950.581 |
| RMSE | 26473.616 |
| R² | 0.880 |
| MAPE | 10.12% |
Заметьте, MAE значительно меньше RMSE. А метрика RMSE более чувствительна к выбросам. Эта особенность говорит нам о наличии аномально больших разрывов между реальными значениями и предсказанными.
Остальные метрики показывают вполне оптимистичное значение.
Вернёмся к аномалиям. Лучший способ наглядно за ними проследить - это построить график. На нём они от нас не убегут. Сопоставим реальные значения с предсказанными используя regplot из seaborn.
plt.figure(figsize=(10, 6))
sns.regplot(x=y_test, y=y_pred, scatter_kws={'alpha':0.4, 'color':'#1f77b4'}, line_kws={'color':'red'})
plt.plot([min(y_test), max(y_test)], [min(y_test), max(y_test)], '--', color='gray')
plt.title(f'LGBM: Истинное vs Предсказанное', pad=20)
plt.xlabel('Истинные значения', labelpad=10)
plt.ylabel('Предсказанные значения', labelpad=10)
plt.grid(alpha=0.3)
plt.show()
А вот и причина высокого RMSE. Модель сильно и систематически занижает стоимость дорогого жилья. Предсказания по жилью низкого и среднего ценового сегмента близки к эталону. Скорее всего причина в недостатке записей с высокой стоимостью жилья. Модели почти не на чем было учиться.
На всякий случай взглянем также на это с другой стороны. Посмотрим распределение ошибок на гистограмме.
plt.figure(figsize=(10, 5))
errors = y_test - y_pred
sns.histplot(errors, kde=True, bins=30, color='#2ca02c')
plt.title('Распределение ошибок предсказания', pad=15)
plt.xlabel('Ошибка (Истинное - Предсказанное)', labelpad=10)
plt.ylabel('Частота', labelpad=10)
plt.axvline(x=0, color='red', linestyle='--')
plt.grid(alpha=0.3)
plt.show()
Распределение близко к нормальному, здесь проблем нет.
Мы проверили качество модели, но пока не рассматривали из чего оно формируется. Проверим какие признаки вносят наибольший вклад в решения модели.
Для интерпритации используем shap-графики. На графике beeswarm рассмотрим общую важность признаков, а на графике waterfall проследим за решением на основе одной конкретной записи.
explainer = shap.TreeExplainer(lgbm_cv.best_estimator_.named_steps['model'])
preprocessor.fit(X_train)
X_test_transformed = pd.DataFrame(
preprocessor.transform(X_test),
columns=preprocessor.get_feature_names_out(),
index=X_test.index
)
shap_values = explainer.shap_values(X_test_transformed)
plt.figure(figsize=(10, 6))
shap.summary_plot(shap_values, X_test_transformed, plot_type="dot", show=False, feature_names=preprocessor.get_feature_names_out())
plt.title('Важность признаков SHAP', pad=20)
plt.tight_layout()
plt.show()
plt.title('Влияние признаков на конкретную запись', pad=20)
shap.plots.waterfall(shap.Explanation(
values=shap_values[42],
base_values=explainer.expected_value,
data=X_test_transformed.iloc[42],
feature_names=preprocessor.get_feature_names_out()
))
Наибольший вклад вносят показатели площади жилья и десятибальная оценка общего качества, что звучит логично. Чем выше показатели обоих признаков - тем модель более склонна отнести жильё к дорогому.
На этом финальное тестирование завершается. Итого, мы:
- Обучили модель на всей тренировочной выборке;
- Вывели показатели четырёх метрик на тестовых данных: MAE, RMSE, R2 и MAPE. По метрикам можно сделать вывод, что модель хорошо справляется со своими обязанностями.
- Построили график соотношения истинных и предсказанных значений тестовых данных, выявили аномальные значения ошибок и систематическое занижение предсказанных значений на дорогом жилье.
- Построили гистограмму распределения ошибок предсказания, убедились в нормальности распределения.
- Построили два SHAP-графика для оценки важности признаков и выявили наиболее ценные признаки -
GrLivAreaиOverallQual
Итоговый вывод¶
В рамках разведочного анализа и предобработки, мы:
- Ознакомились с данными, выявили потенциальные проблемы и малоинформативные признаки;
- Подготовили инструменты автоматической предобработки данных.
В рамках обучения моделей, мы:
- Обучили, подобрали параметры и провели кросс-валидацию для трёх разнообразных моделей машинного обучения;
- Выявили лучшую модель - Лёгкий градиентный бустинг.
В рамках итогового тестирования, мы:
- Ознакомились с метриками MAE, RMSE, R2 и MAPE;
- Выявили проблему недостатка данных о дорогом жилье в выборке;
- Выявили наиболее информативные признаки - размер жилья и общая оценка качества;
Рекомендации заказчику:
- Дополнить данные записями о более разнообразном по стоимости жилье для улучшения качества предсказаний.